/*
    BEEM is a videoconference application on the Android Platform.

    Copyright (C) 2009 by Frederic-Charles Barthelery,
                          Jean-Manuel Da Silva,
                          Nikita Kozlov,
                          Philippe Lago,
                          Jean Baptiste Vergely,
                          Vincent Veronis.

    This file is part of BEEM.

    BEEM is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    BEEM is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with BEEM.  If not, see <http://www.gnu.org/licenses/>.

    Please send bug reports with examples or suggestions to
    contact@beem-project.com or http://dev.beem-project.com/
 */

package com.beem.project.beem;

import java.io.IOException;
import java.util.ArrayList;

import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.RosterGroup;
import org.jivesoftware.smack.packet.Presence;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.util.Log;

import com.beem.project.beem.service.BeemAvatarCache;
import com.beem.project.beem.ui.BeemNotification;
import com.beem.project.beem.utils.Status;

/**
 * Roster synchronisation, informations in database.
 * @author Vincent V. <marseille@beem-project.com>
 */
public class BeemSync {

    private static final String TAG = "BeemSync";
    private static final int NB_DB_OPERATION = 50;

    private Context mContext;

    /**
     * Constructor.
     * @param c the context parameter.
     */
    public BeemSync(Context c) {
	mContext = c;
    }

    /**
     * Method to execute content provider operation.
     * @param ops
     */
    private void executeOperation(final ArrayList<ContentProviderOperation> ops) {
	try {
	    mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
	} catch (RemoteException e) {
	    Log.d(TAG, "Error during sync of contact", e);
	} catch (OperationApplicationException e) {
	    Log.d(TAG, "Error during sync of contact", e);
	}
	ops.clear();
    }

    /**
     * Roster sync method.
     * @param r The roster to sync
     * @param a The account related
     */
    public void manageRoster(final Roster r, final String accountName) {
	if (r == null) {
	    Log.w(TAG, "Roster is null cant sync informations");
	    return;
	}
	ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
	for (RosterGroup group : r.getGroups()) {
	    if (group != null) {
		manageGroup(ops, accountName, group);
	    }
	    if (ops.size() > NB_DB_OPERATION)
		executeOperation(ops);
	}
	if (ops.size() > 0)
	    executeOperation(ops);
	for (RosterEntry entry : r.getEntries()) {
	    if (entry != null) {
		long rawContactID = manageEntry(ops, accountName, entry);
		addUpdateStatus(ops, entry, r.getPresence(entry.getUser()), rawContactID);
	    }
	    if (ops.size() > NB_DB_OPERATION)
		executeOperation(ops);
	}
	if (ops.size() > 0)
	    executeOperation(ops);
    }

    private void manageGroup(ArrayList<ContentProviderOperation> ops, String accountName, RosterGroup group) {
	Log.i(TAG, "Sync group : " + group.getName() + " " + group.getEntryCount());
	long rawGroupID = getRawGroupID(accountName, group.getName());
	if (rawGroupID == -1) {

	    ContentProviderOperation.Builder builder = ContentProviderOperation
		.newInsert(ContactsContract.Groups.CONTENT_URI);
	    Account[] accounts = AccountManager.get(mContext).getAccounts();
	    for (Account account : accounts) {
		if (account.name.equals(accountName)) {
		    builder.withValue(ContactsContract.Groups.ACCOUNT_NAME, account.name);
		    builder.withValue(ContactsContract.Groups.ACCOUNT_TYPE, account.type);
		    break;
		}
	    }
	    builder.withValue(ContactsContract.Groups.TITLE, group.getName());
	    ops.add(builder.build());
	}
    }

    /**
     * RosterEntry sync method.
     * @param ops The content provider operation
     * @param account The account related
     * @param entry The roster entry to sync
     * @return The raw contact ID
     */
    private long manageEntry(ArrayList<ContentProviderOperation> ops, String accountName, RosterEntry entry) {
	long rawContactID = getRawContactID(accountName, entry.getUser());
	Log.i(TAG, "Sync Contact : " + entry.getUser() + " RawContactID : " + rawContactID);
	if (rawContactID == -1) { // Not found in database, add new
	    ContentValues values = new ContentValues();
	    Account[] accounts = AccountManager.get(mContext).getAccounts();
	    for (Account account : accounts) {
		if (account.name.equals(accountName)) {
		    values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type);
		    values.put(ContactsContract.RawContacts.ACCOUNT_NAME, account.name);
		    break;
		}
	    }
	    values.put(ContactsContract.RawContacts.SOURCE_ID, entry.getUser());
	    Uri rawContactUri = mContext.getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);
	    rawContactID = ContentUris.parseId(rawContactUri);
	    values.clear();
	    ContentProviderOperation.Builder builder = addUpdateStructuredName(entry, rawContactID, true);
	    ops.add(builder.build());
	    for (RosterGroup group : entry.getGroups()) {
		builder = addUpdateGroup(entry, rawContactID, getRawGroupID(accountName, group.getName()), true);
		ops.add(builder.build());
	    }
	    builder = createProfile(entry, rawContactID, accountName);
	    ops.add(builder.build());
	} else { // Found, update
	    ContentProviderOperation.Builder builder = addUpdateStructuredName(entry, rawContactID, false);
	    ops.add(builder.build());
	    // TODO: ADD AVATAR
	    // builder = addUpdatePhoto(entry, rawContactID, false);
	    // ops.add(builder.build());
	}
	return rawContactID;
    }

    /**
     * Method to insert or update structured name informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private ContentProviderOperation.Builder addUpdateStructuredName(RosterEntry entry, long rawContactID,
	boolean isInsert) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	ContentProviderOperation.Builder builder;
	if (isInsert) {
	    builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	    builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	    builder.withValue(ContactsContract.Data.MIMETYPE,
		ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
	    builder.withValue(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, rawContactID);
	} else {
	    builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI);
	    builder.withSelection(
		ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID + " =? AND "
		    + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
		    + "'", new String[] { String.valueOf(rawContactID) });
	}
	builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName);
	builder.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, displayName);
	builder.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, displayName);
	return builder;
    }

    /**
     * Method to insert or update avatar.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private ContentProviderOperation.Builder addUpdatePhoto(RosterEntry entry, long rawContactID, boolean isInsert) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	ContentProviderOperation.Builder builder;
	if (isInsert) {
	    builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	    builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	    builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
	    builder.withValue(ContactsContract.CommonDataKinds.Photo.RAW_CONTACT_ID, rawContactID);
	} else {
	    builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI);
	    builder.withSelection(ContactsContract.CommonDataKinds.Photo.RAW_CONTACT_ID + " =? AND "
		+ ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE
		+ "'", new String[] { String.valueOf(rawContactID) });
	}
	BeemAvatarCache bac = new BeemAvatarCache(mContext);
	try {
	    builder.withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, bac.get(entry.getUser()));
	} catch (IOException e) {
	    Log.e(TAG, "Error cache beem avatar", e);
	}
	return builder;
    }

    /**
     * Method to insert or update group name informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param rawGroupID The group ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private ContentProviderOperation.Builder addUpdateGroup(RosterEntry entry, long rawContactID, long rawGroupID,
	boolean isInsert) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	Log.e(TAG + "UPDATE GROUP", "Contact : " + displayName + " GroupID :" + rawGroupID);
	ContentProviderOperation.Builder builder = null;
	if (isInsert) {
	    builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	    builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	    builder.withValue(ContactsContract.Data.MIMETYPE,
		ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE);
	    builder.withValue(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, rawGroupID);
	}
	// TODO: delete - contact doesnt appear anymore in this group
	return builder;
    }

    /**
     * Method to insert or update IM informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private ContentProviderOperation.Builder createProfile(RosterEntry entry, long rawContactID, String accountName) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	ContentProviderOperation.Builder builder;
	builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
	builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactID);
	builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
	builder.withValue(ContactsContract.CommonDataKinds.Im.RAW_CONTACT_ID, rawContactID);
	builder.withValue(ContactsContract.CommonDataKinds.Im.DATA1, displayName);
	builder.withValue(ContactsContract.CommonDataKinds.Im.PROTOCOL,
	    ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER);
	return builder;
    }

    /**
     * Method to insert or update IM informations.
     * @param entry The roster entry to sync
     * @param rawContactID The contact ID in the android database
     * @param isInsert Insert boolean
     * @return
     */
    private void addUpdateStatus(ArrayList<ContentProviderOperation> ops, RosterEntry entry, Presence p,
	long rawContactID) {
	String displayName = entry.getName() != null ? entry.getName() : entry.getUser();
	Log.i(TAG + "UPDATESTATUS", "Contact : " + displayName + " Presence status : " + p.getStatus()
	    + " Presence status state : " + Status.getStatusFromPresence(p));
	ContentProviderOperation.Builder builder;
	builder = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI);
	builder.withValue(ContactsContract.StatusUpdates.PROTOCOL, ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER);
	builder.withValue(ContactsContract.StatusUpdates.IM_HANDLE, displayName);
	// TODO: Get account name
	builder.withValue(ContactsContract.StatusUpdates.IM_ACCOUNT, "beem@elyzion.net");
	builder.withValue(ContactsContract.StatusUpdates.STATUS, p.getStatus());
	builder.withValue(ContactsContract.StatusUpdates.STATUS_RES_PACKAGE, "com.beem.project.beem");
	builder.withValue(ContactsContract.StatusUpdates.STATUS_LABEL, R.string.app_name);
	// TODO: Get status icon
	builder.withValue(ContactsContract.StatusUpdates.STATUS_ICON, R.drawable.beem_status_icon);
	// TODO: Pb presence ... 2 appear on 3 raw .... random appear
	builder.withValue(ContactsContract.StatusUpdates.PRESENCE, Status.getStatusFromPresence(p));
	ops.add(builder.build());
    }

    /**
     * Get contact ID from android database.
     * @param account The account related
     * @param jid The jid related
     * @return ID in the database of the jid
     */
    private long getRawContactID(String account, String jid) {
	long authorId = -1;
	final Cursor c = mContext.getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,
	    new String[] { ContactsContract.RawContacts._ID, ContactsContract.RawContacts.SOURCE_ID },
	    ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " + ContactsContract.RawContacts.SOURCE_ID + "=?",
	    new String[] { account, jid }, null);
	try {
	    if (c.moveToFirst())
		authorId = c.getInt(c.getColumnIndex(ContactsContract.RawContacts._ID));
	} finally {
	    if (c != null)
		c.close();
	}
	return authorId;
    }

    /**
     * Get group ID from android database.
     * @param account The account related
     * @param group The group related
     * @return ID in the database of the jid
     */
    private long getRawGroupID(String account, String group) {
	long authorId = -1;
	final Cursor c = mContext.getContentResolver().query(ContactsContract.Groups.CONTENT_URI,
	    new String[] { ContactsContract.Groups._ID },
	    ContactsContract.Groups.ACCOUNT_NAME + "=? AND " + ContactsContract.Groups.TITLE + "=?",
	    new String[] { account, group }, null);
	try {
	    if (c.moveToFirst())
		authorId = c.getInt(c.getColumnIndex(ContactsContract.Groups._ID));
	} finally {
	    if (c != null)
		c.close();
	}
	return authorId;
    }
}
